package vh.sphynx.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class CharSequenceStream implements
		CharSequence {
	
	class Buffer {
		public byte data[];
		public int location;
		public int size;
		
		public Buffer(int capacity) {
			data = new byte[capacity];
			location = size = 0;
		}
		
		public boolean hit(int location, int len) {
			return len == 1? location >= this.location && location <= this.location + size - 1:
				hit(location, 1) && hit(location + len - 1, 1); 
		}
		
		public void fillBuffer(int position) throws IOException {
			int len = data.length >> 1;
			int mid = len;
			if (location + size == position) {
				// the requested position is next buffer, so advance to buffer by half of buf len
				// so that requested position is at the middle of the buffer.
				System.arraycopy(data, len, data, 0, len);
				this.location = position - mid;
				file.seek(position);
				size = mid + file.read(data, mid, len);
			}
			else {
				// the request position is arbitrary, so calculate the offset so that the request position
				// is at the middle of the buffer
				this.location = Math.max(offset, position - mid);
				file.seek(location);
				size = file.read(data);
			}
		}

		public byte get(int index) {
			return data[index - location];
		}

		public byte[] get(int start, int len) {
			byte buf[] = new byte[len];
			System.arraycopy(data, start - location, buf, 0, len);
			return buf;
		}
	}
	public final int offset, length;
	protected RandomAccessFile file;
	protected Buffer buffer;
	
	public CharSequenceStream(String name) throws IOException {
		this(name, 4096);
	}
	
	public CharSequenceStream(File name) throws IOException {
		this(name, 4096);
	}
	
 	public CharSequenceStream(String name, Integer bufferCapacity)
			throws IOException {
		file = new RandomAccessFile(new File(name).getAbsolutePath(), "r");
		buffer = new Buffer(bufferCapacity);
		offset = 0;
		length = (int)file.length();
	}

	public CharSequenceStream(File file, Integer bufferCapacity)
			throws FileNotFoundException {
		this.file = new RandomAccessFile(file.getAbsoluteFile(), "r");
		buffer = new Buffer(bufferCapacity);
		offset = 0;
		length = (int)file.length();
	}
	
	public CharSequenceStream(CharSequenceStream source, int offset, int len) {
		this.file = source.file;
		this.buffer = new Buffer(source.buffer.data.length);
		this.offset = offset;
		this.length = len;
	}
	
	@Override
	public char charAt(int index) {
		if (index > length)
			throw new IndexOutOfBoundsException();
		
		int newidx = index + offset;
		
		if (!buffer.hit(newidx, 1))
			try {
				buffer.fillBuffer(newidx);
			} catch (IOException e) {
				e.printStackTrace();
			}
		char ch = (char)buffer.get(newidx);
		return ch;
	}

	@Override
	public CharSequence subSequence(int start, int end) {
		if (end > length || start > length)
			throw new IndexOutOfBoundsException("index exceeds out of length.");
		
		start += offset;
		end += offset;
		int len = end - start;
		
		if (len <= 10 * 1024) // max 40K string 
			if (buffer.hit(start, end - start)) {
				byte buf[] = buffer.get(start, end - start);
				return new ArrayCharSequence(buf);
			}
			else {
				try {
					file.seek(start);
					byte buf[] = new byte[end - start];
					file.read(buf);
					return new ArrayCharSequence(buf);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		else
			return new CharSequenceStream(this, start, len);
		return null;
	}
	
	public String toString(boolean forceLoad) {
		if (forceLoad || length < 100) {
			return length == 0 ? "" : new StringBuilder(length).append(this, 0, length-1).toString();
		}
		return String.format("CharSequenceStream(offset: %d, length: %d)", offset, length);
	}
	
	@Override
	public String toString() {
		return this.toString(true);
	}
	
	@Override
	public int length() {
		return length;
	}

	public void close() {
		try {
			file.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
